/******************************************************************************* * Copyright (c) 2004, 2015 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.ui.internal.dnd; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.jface.util.Geometry; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Tracker; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.internal.DragCursors; /** * Provides the methods for attaching drag-and-drop listeners to SWT controls. */ public class DragUtil { private static final String DROP_TARGET_ID = "org.eclipse.ui.internal.dnd.dropTarget"; //$NON-NLS-1$ /** * The location where all drags will end. If this is non-null, then * all user input is ignored in drag/drop. If null, we use user input * to determine where objects should be dropped. */ private static TestDropLocation forcedDropTarget = null; /** * List of IDragOverListener */ private static List defaultTargets = new ArrayList(); /** * Sets the drop target for the given control. It is possible to add one or more * targets for a "null" control. This becomes a default target that is used if no * other targets are found (for example, when dragging objects off the application * window). * * @param control the control that should be treated as a drag target, or null * to indicate the default target * @param target the drag target to handle the given control */ public static void addDragTarget(Control control, IDragOverListener target) { if (control == null) { defaultTargets.add(target); } else { List targetList = getTargetList(control); if (targetList == null) { targetList = new ArrayList(1); } targetList.add(target); control.setData(DROP_TARGET_ID, targetList); } } /** * Return the list of 'IDragOverListener' elements associated with * the given control. If there's a 'global' listener then always * return it. * * @param control * @return */ private static List getTargetList(Control control) { List result = (List) control.getData(DROP_TARGET_ID); return result; } /** * Removes a drop target from the given control. * * @param control * @param target */ public static void removeDragTarget(Control control, IDragOverListener target) { if (control == null) { defaultTargets.remove(target); } else { List targetList = getTargetList(control); if (targetList != null) { targetList.remove(target); if (targetList.isEmpty()) { control.setData(DROP_TARGET_ID, null); } } } } /** * Shorthand method. Returns the bounding rectangle for the given control, in * display coordinates. Note that all 'Shell' controls are expected to be 'top level' * so DO NOT do the origin offset for them. * * @param draggedItem * @param boundsControl * @return */ public static Rectangle getDisplayBounds(Control boundsControl) { Control parent = boundsControl.getParent(); if (parent == null || boundsControl instanceof Shell) { return boundsControl.getBounds(); } return Geometry.toDisplay(parent, boundsControl.getBounds()); } public static boolean performDrag(final Object draggedItem, Rectangle sourceBounds, Point initialLocation, boolean allowSnapping) { IDropTarget target = dragToTarget(draggedItem, sourceBounds, initialLocation, allowSnapping); if (target == null) { return false; } target.drop(); // If the target can handle a 'finished' notification then send one if (target!= null && target instanceof IDropTarget2) { ((IDropTarget2)target).dragFinished(true); } return true; } /** * Drags the given item to the given location (in display coordinates). This * method is intended for use by test suites. * * @param draggedItem object being dragged * @param finalLocation location being dragged to * @return true iff the drop was accepted */ public static boolean dragTo(Display display, Object draggedItem, Point finalLocation, Rectangle dragRectangle) { Control currentControl = SwtUtil.findControl(display, finalLocation); IDropTarget target = getDropTarget(currentControl, draggedItem, finalLocation, dragRectangle); if (target == null) { return false; } target.drop(); return true; } /** * Forces all drags to end at the given position (display coordinates). Intended * for use by test suites. If this method is called, then all subsequent calls * to performDrag will terminate immediately and behave as though the object were * dragged to the given location. Calling this method with null cancels this * behavior and causes performDrag to behave normally. * * @param forcedLocation location where objects will be dropped (or null to * cause drag/drop to behave normally). */ public static void forceDropLocation(TestDropLocation forcedLocation) { forcedDropTarget = forcedLocation; } /** * Drags the given item, given an initial bounding rectangle in display coordinates. * Due to a quirk in the Tracker class, changing the tracking rectangle when using the * keyboard will also cause the mouse cursor to move. Since "snapping" causes the tracking * rectangle to change based on the position of the mouse cursor, it is impossible to do * drag-and-drop with the keyboard when snapping is enabled. * * @param draggedItem object being dragged * @param sourceBounds initial bounding rectangle for the dragged item * @param initialLocation initial position of the mouse cursor * @param allowSnapping true iff the rectangle should snap to the drop location. This must * be false if the user might be doing drag-and-drop using the keyboard. * * @return */ static IDropTarget dragToTarget(final Object draggedItem, final Rectangle sourceBounds, final Point initialLocation, final boolean allowSnapping) { final Display display = Display.getCurrent(); // Testing...immediately 'drop' onto the test target if (forcedDropTarget != null) { Point location = forcedDropTarget.getLocation(); Control currentControl = SwtUtil.findControl(forcedDropTarget.getShells(), location); return getDropTarget(currentControl, draggedItem, location, sourceBounds); } // Create a tracker. This is just an XOR rect on the screen. // As it moves we notify the drag listeners. final Tracker tracker = new Tracker(display, SWT.NULL); tracker.setStippled(true); tracker.addListener(SWT.Move, event -> display.syncExec(() -> { // Get the curslor location as a point Point location = new Point(event.x, event.y); // Select a drop target; use the global one by default IDropTarget target = null; Control targetControl = display.getCursorControl(); // Get the drop target for this location target = getDropTarget(targetControl, draggedItem, location, tracker.getRectangles()[0]); // Set up the tracker feedback based on the target Rectangle snapTarget = null; if (target != null) { snapTarget = target.getSnapRectangle(); tracker.setCursor(target.getCursor()); } else { tracker.setCursor(DragCursors .getCursor(DragCursors.INVALID)); } // If snapping then reset the tracker's rectangle based on the current drop target if (allowSnapping) { if (snapTarget == null) { snapTarget = new Rectangle(sourceBounds.x + location.x - initialLocation.x, sourceBounds.y + location.y - initialLocation.y, sourceBounds.width, sourceBounds.height); } // Try to prevent flicker: don't change the rectangles if they're already in // the right location Rectangle[] currentRectangles = tracker.getRectangles(); if (!(currentRectangles.length == 1 && currentRectangles[0] .equals(snapTarget))) { tracker.setRectangles(new Rectangle[] { Geometry.copy(snapTarget) }); } } })); // Setup...when the drag starts we might already be over a valid target, check this... // If there is a 'global' target then skip the check IDropTarget target = null; Control startControl = display.getCursorControl(); if (startControl != null && allowSnapping) { target = getDropTarget(startControl, draggedItem, initialLocation, sourceBounds); } // Set up an initial tracker rectangle Rectangle startRect = sourceBounds; if (target != null) { Rectangle rect = target.getSnapRectangle(); if (rect != null) { startRect = rect; } tracker.setCursor(target.getCursor()); } if (startRect != null) { tracker.setRectangles(new Rectangle[] { Geometry.copy(startRect)}); } // Tracking Loop...tracking is preformed on the 'SWT.Move' listener registered // against the tracker. // HACK: // Some control needs to capture the mouse during the drag or other // controls will interfere with the cursor Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); if (shell != null) { shell.setCapture(true); } // Run tracker until mouse up occurs or escape key pressed. boolean trackingOk = tracker.open(); // HACK: // Release the mouse now if (shell != null) { shell.setCapture(false); } // Done tracking... // Get the current drop target IDropTarget dropTarget = null; Point finalLocation = display.getCursorLocation(); Control targetControl = display.getCursorControl(); dropTarget = getDropTarget(targetControl, draggedItem, finalLocation, tracker.getRectangles()[0]); // Cleanup... tracker.dispose(); // if we're going to perform a 'drop' then delay the issuing of the 'finished' // callback until after it's done... if (trackingOk) { return dropTarget; } else if (dropTarget!= null && dropTarget instanceof IDropTarget2) { // If the target can handle a 'finished' notification then send one ((IDropTarget2)dropTarget).dragFinished(false); } return null; } /** * Given a list of IDragOverListeners and a description of what is being dragged, it returns * a IDropTarget for the current drop. * * @param toSearch * @param mostSpecificControl * @param draggedObject * @param position * @param dragRectangle * @return */ private static IDropTarget getDropTarget(List toSearch, Control mostSpecificControl, Object draggedObject, Point position, Rectangle dragRectangle) { if (toSearch == null) { return null; } Iterator iter = toSearch.iterator(); while (iter.hasNext()) { IDragOverListener next = (IDragOverListener) iter.next(); IDropTarget dropTarget = next.drag(mostSpecificControl, draggedObject, position, dragRectangle); if (dropTarget != null) { return dropTarget; } } return null; } /** * Returns the drag target for the given control or null if none. * * @param toSearch * @param e * @return */ public static IDropTarget getDropTarget(Control toSearch, Object draggedObject, Point position, Rectangle dragRectangle) { // Search for a listener by walking the control's parent hierarchy for (Control current = toSearch; current != null; current = current .getParent()) { IDropTarget dropTarget = getDropTarget(getTargetList(current), toSearch, draggedObject, position, dragRectangle); if (dropTarget != null) { return dropTarget; } // Don't look to parent shells for drop targets if (current instanceof Shell) { break; } } // No controls could handle this event -- check for default targets return getDropTarget(defaultTargets, toSearch, draggedObject, position, dragRectangle); } /** * Returns the location of the given event, in display coordinates * @return */ public static Point getEventLoc(Event event) { Control ctrl = (Control) event.widget; return ctrl.toDisplay(new Point(event.x, event.y)); } }